I.傳值與傳參考
II. 傳參考與closure (1) --> now
II. 傳參考與closure (2)
II. 傳參考與closure (3)
當我發現我今天必須要寫 closure 的時候,我覺得我瘋了,真的是沒事給自己找事做...
由於 javascript 並不是我第一個學習的程式語言,我相信對很多人來說都是這樣,在其他的語言中,例如 C/C++,甚至是 PHP,對於函式的執行大約是維持了 stack 的結構,每次呼叫函式,就將即將要被執行的函式 push 進去 stack 中,函式中還有再呼叫函式的,就再 push 進去,當正在執行中的這個函式結束或是 return 的時候,就會 pop 出去,將回傳值傳到目前 stack 最上面的那個函式中,利用這個 stack,我們就可以用來記憶目前函式呼叫的順序與層級。
如上圖所示,main函式呼叫了fun1,func1 呼叫了 func2,當 func2 執行完畢後,就會從這個 stack 中 pop 出去,整塊記憶體清得乾乾淨淨,這時候 func1 如果又接著呼叫了 func3,func3 就會再 push 進去,蓋掉那塊記憶體了。由此可知,當執行結束的那個函式被pop掉時,當初在那個函式中的所有區域變數也都不見了。
但是,在 javascript 中,事情好像沒有這麼單純,於是李組長眉頭一皺的看了這個範例:
function foo(){
var temp = 'azole';
function bar(){
console.log(temp);
}
return bar;
}
var myFunc = foo();
myFunc(); // azole
咦?咦?咦?當 foo 被呼叫時,建立了一個 temp 區域變數,然後回傳了 bar 這個函式給 myFunc,再然後 foo 就結束了,而作為 foo 的區域變數的 temp,應該就要跟著死掉了,為什麼當我們執行 myFunc 時,還能正確的 log 出 azole 呢?好見鬼阿!
這個就是傳說中的 javascript 的 closure 了,一般對 closure 的解釋大約是內層函式可以取用外層函式定義的變數,或者是可以取用所處環境的變數,這邊我強調是“一般”的說法,因為,單純的使用“取用”或“存取”這兩個字,是無法完全理解 closure 的。
在繼續討論之前,我們先來看一個例子,這是一個很經典的、大家都踩過的 closure 的雷:
for(var i=0; i<10;i++){
setTimeout(function(){
console.log(i);
}, 500);
}
這裡頭的console並不如預期中的寫出 0, 1, ..., 9,而且寫出了 10 個 10,為什麼為什麼為什麼呢?不是說內層可以取用外層變數嗎?為什麼會是 10 ?
正因為以上這個範例(及其類似的範例),我這個傻人對"一般"的定義一直非常的疑惑與不解,一直到我開始看了 Javascript: The Good Parts 與 Effective Javascript 這兩本書之後,才開始有了一點點理解(也因此才有這篇文章)。
Javascript: The Good Parts 中舉的範例如下:
var quo = function(status){
return {
get_status: function(){
return status;
}
};
};
var aQuo = quo('azole');
console.log(aQuo.get_status());
文中的解釋是:
get_status對參數複本(a copy to parameter)沒有存取權,而是對參數本身有存取權。都是因為函式能取用建造它本身的背景情境,才有這種可能。這種狀況稱為 closure(閉包)。
來源 Javascript: The Good Parts 中文版 第 39 頁
我必須說,Javascript: The Good Parts 這本真的是超級好書,要學 javascript 的人一定要讀讀,這本書精簡、有力、字字珠璣,但真的太字字珠璣了,我到這邊還沒有真的看懂,秉持着「人一能之,己百之。人十能之,己千之。果能此道矣,雖愚必明,雖柔必強。」的精神,我多看了十遍,但還是不是真的很理解,有點似懂非懂,還好這時候,還有另外一本書 Effective Javascript 拯救了我。
Effective Javascript 中是這樣說的:
- Javascript 允許你**參考(refer to)**定義在目前函式外的變數。
- 外層函數回傳(return)之後,內層函式也能參考定義在那些外層函式中的變數。
- 它們也能更新外層變數的值。
看到這邊,有沒有發現!?最重要的關鍵字是什麼,就是「參考」這兩個字阿,這時候 Effective Javascript 怕大家還不夠明白,再補上殺手級的一句:Closures實際上儲存的是對那些外層變數的參考(references)。
到這裡,看到參考二字,才真正的明白 Javascript: The Good Parts 中所強調的,對參數複本沒有存取權(這個參數複本指的應該就是值),而是對參數本身(也就是藉由儲存這個參數的參考來去參考到這個參數本身)有存取權。
到此,如果你有看懂,我為何要反覆強調「參考」二字的話,你應該就會知道為什麼那個錯誤的範例總是印出 10,而不是印出我門想要的 0, 1, ..., 9 了,如果不明白也沒關係,明天會講解。(我快沒梗了,給我留一點明天寫。)